// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SerializedFileReader.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Runtime
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.IO;
    using System.Runtime.Serialization;
    using System.Threading;
    using System.Xml;
    using Microsoft.Robotics.IO;

    /// <summary>
    /// Interface that makes it possible to use the FilePlayerAgent with multiple file types (e.g. external dataset files)
    /// </summary>
    public interface IFileReader
    {
        /// <summary>
        /// Initializes the file reader.
        /// </summary>
        /// <param name="filePath">The path to the file to read</param>
        /// <param name="publishingQueue">Deserialized messages are added to this queue.</param>
        /// <returns>The time origin of the file</returns>
        long Initialize(string filePath, BlockingCollection<AgentMessage> publishingQueue);

        /// <summary>
        /// Starts deserializing messages and adding them to the publishing queue. 
        /// This method blocks until the file is completely read or until Stop is called.
        /// </summary>
        void Start();

        /// <summary>
        /// Cancels the file reading operation. This is an async operation
        /// </summary>
        void Stop();
    }

    /// <summary>
    /// A reader that reads the message, deserializes it, and sends to a forwarder
    /// </summary>
    public class SerializedFileReader : IFileReader, IDisposable
    {
        /// <summary>
        /// The FileStream to open and write 
        /// </summary>
        private FileStream fileStream;

        /// <summary>
        /// The XMLReader used to read the stream
        /// </summary>
        private XmlReader reader;

        /// <summary>
        /// The data contract serializer to use.
        /// </summary>
        private DataContractSerializer serializer;

        /// <summary>
        /// The queue of deserialized messages waiting to be published at the right time
        /// </summary>
        private BlockingCollection<AgentMessage> publishingQueue;

        /// <summary>
        /// Indicates if the reader has more messages to publish.
        /// </summary>
        private CancellationTokenSource cancellationSource;

        /// <summary>
        /// Indicates if the file is in binary xml format.
        /// </summary>
        private bool binarySerialization;

        /// <summary>
        /// Initializes a new instance of the SerializedFileReader class.
        /// </summary>
        /// <param name="messageTypes">The types of messages we expect to deserialize</param>
        /// <param name="binarySerialization">Indicates if the file is in binary xml format.</param>
        public SerializedFileReader(IEnumerable<Type> messageTypes, bool binarySerialization)
        {
            this.binarySerialization = binarySerialization;
            this.serializer = new DataContractSerializer(
                typeof(AgentMessage),
                messageTypes,
                2048, // TODO: reduce back to 256 - Bug 1836 Add Pose2D to SerailableArray list
                true,
                false,
                new SerializationSurrogate(true));
            this.cancellationSource = new CancellationTokenSource();
        }

        /// <summary>
        /// Initializes a new instance of the SerializedFileReader class
        /// </summary>
        /// <param name="filePath">The file path</param>
        /// <param name="publishingQueue">The queue where to store the messages deserialized so far</param>
        /// <returns>The recording start time, if persisted in the file</returns>
        public long Initialize(string filePath, BlockingCollection<AgentMessage> publishingQueue)
        {
            this.publishingQueue = publishingQueue;
            
            long recordingStartTime = 0;
            this.fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);

            if (this.binarySerialization)
            {
                this.reader = XmlDictionaryReader.CreateBinaryReader(this.fileStream, XmlDictionaryReaderQuotas.Max);
            }
            else
            {
                this.reader = XmlReader.Create(this.fileStream);
            }

            // Read the root node and get the delay value (time origin shift) if present, If not, assume 0.
            this.reader.Read();
            if (this.reader.HasAttributes)
            {
                string value = this.reader.GetAttribute("start");
                recordingStartTime = long.Parse(value);
            }

            return recordingStartTime;
        }

        /// <summary>
        /// Deserializes messages and inserts them in the publishing queue.
        /// This method blocks until Stop is called or until the end of file is reached.
        /// </summary>
        public void Start()
        {
            this.cancellationSource = new CancellationTokenSource();
            CancellationToken cancellationToken = this.cancellationSource.Token;
            using (this.fileStream)
            {
                // loop over all entries in the file and add them to the queue.
                // the queue will block if we are going too fast.
                this.reader.Read();
                try
                {
                    while (this.reader.Name != "root" && !cancellationToken.IsCancellationRequested)
                    {
                        try
                        {
                            AgentMessage message = (AgentMessage)this.serializer.ReadObject(this.reader);
                            this.publishingQueue.Add(message, cancellationToken);
                        }
                        catch (SerializationException)
                        {
                            // ignore the error in the hope that the next item is ok
                            Console.WriteLine("Invalid XML deserializing file {0}", this.fileStream.Name);
                        }
                    }
                }
                catch (OperationCanceledException)
                {
                    // do nothing, the agent cancelled the operation
                }

                this.publishingQueue.CompleteAdding();
            }
        }

        /// <summary>
        /// Stops listening for messages
        /// </summary>
        public void Stop()
        {
            this.cancellationSource.Cancel();
        }

        /// <summary>
        /// Disposes of an instance of the <see cref="SerializedFileReader"/> class.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
        }

        /// <summary>
        /// Disposes of an instance of the <see cref="SerializedFileReader"/> class.
        /// </summary>
        /// <param name="disposing">True if disposing, false if finalizing.</param>
        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.cancellationSource != null)
                {
                    this.cancellationSource.Dispose();
                }

                if (this.fileStream != null)
                {
                    this.fileStream.Dispose();
                }
            }
        }
    }
}
